Hierarchically coupled causal models#

Here, we showcase how to generate hierarchically coupled causal models.

import torch
import xarray as xr
import matplotlib as mpl
import matplotlib.pyplot as plt

from causaldynamics.utils import set_rng_seed
from causaldynamics.initialization import initialize_weights, initialize_biases, initialize_x
from causaldynamics.scm import create_scm_graph, get_root_nodes_mask, GrowingNetworkWithRedirection
from causaldynamics.mlp import propagate_mlp
from causaldynamics.systems import solve_system
from causaldynamics.plot import plot_trajectories, animate_3d_trajectories, plot_scm, plot_3d_trajectories
# Set a fixed seed for reproducibility
set_rng_seed(42)

Generate data – the convenient way#

This is the convenient way of generating data. If you need more flexibility, look at the step by step guide below.

This code will first create an structural causal model. The create_scm function returns the adjacency matrix A of the SCM, the weights W and biasses b of all the MLPs located on the nodes and the root_nodes that act as temporal system driving functions.

We use this SCM to simulate a system constisting of num_nodes for num_timesteps and driven by the dynamical systems system_name that are located on the root nodes.

At least, we visualize the results.

from causaldynamics.creator import create_scm, simulate_system, create_plots

num_nodes = 2
node_dim = 3
num_timesteps = 1000

system_name='Lorenz'
confounders = False

A, W, b, root_nodes, _ = create_scm(num_nodes, node_dim, confounders=confounders)

data = simulate_system(A, W, b, 
                      num_timesteps=num_timesteps, 
                      num_nodes=num_nodes,
                      system_name=system_name) 

create_plots(
            data,
            A,
            root_nodes=root_nodes,
            out_dir='.',
            show_plot=True,
            save_plot=False,
            return_html_anim=True,
            create_animation=False,
        )
INFO - Creating SCM with 2 nodes and 3 dimensions each...
INFO - Simulating Lorenz system for 1000 timesteps...
INFO - Generating visualizations
../_images/74c4f6d5b36785a86d9e8ae14e70f603a5f60ee59b40a0ede088da95e93df645.png ../_images/d7e67f1af6f5039d5224513eacfadd319b4cc4a3431063358c239e8334849613.png ../_images/3433017adec1e77dfbd5fab4d42bffb5cc8c2d2887b45b293fa0733fc7521324.png

We provide several features that can be used conveniently accessed through the create_scm and simulate_system functions:

from causaldynamics.creator import create_scm, simulate_system
from causaldynamics.plot import plot_scm, plot_trajectories, plot_3d_trajectories

num_nodes = 5
node_dim = 3
num_timesteps = 1000

confounders = False    # set to True to add scale-free confounders
standardize = False    # set to True to standardize the data
init_ratios = [1, 1]   # set ratios of dynamical systems and periodic drivers at root nodes. Here: equal ratio.
system_name='random'   # sample random dynamical system for the root nodes
noise = 0.5            # set noise for the dynamical systems

time_lag = 10                   # set time lag for time-lagged edges
time_lag_edge_probability = 0.1 # set probability of time-lagged edges


A, W, b, root_nodes, _ = create_scm(num_nodes, 
                                    node_dim,
                                    confounders=confounders,
                                    time_lag=time_lag,
                                    time_lag_edge_probability=time_lag_edge_probability)

data = simulate_system(A, W, b, 
                      num_timesteps=num_timesteps, 
                      num_nodes=num_nodes,
                      system_name=system_name,
                      init_ratios=init_ratios,
                      time_lag=time_lag,
                      standardize=standardize,
                      make_trajectory_kwargs={'noise': noise}) 

plot_scm(G=create_scm_graph(A), root_nodes=root_nodes)
plot_3d_trajectories(data, root_nodes, line_alpha=1.)
plot_trajectories(data, root_nodes=root_nodes, sharey=False)
plt.show()
INFO - Creating SCM with 5 nodes and 3 dimensions each...
INFO - Simulating random system for 1000 timesteps...
../_images/6c157433f9d9ba594077663aac2a6f748ada7a09181654af1aaee5aac9ee39de.png ../_images/a1b528912bfcb0a3522386edc18a6f33cfee87e20bca3275760c5920032fb47a.png ../_images/c209908093bb78d1f2f3059d47d3c74b532749360815c5e53b6f2209b1892220.png

For an overview of the features, have a look at the feature.ipynb notebook. If you want more information on specific features, look at their respective notebook, e.g., driver.ipynb or time_lag.ipynb.

Detailed step by step explanation#

Let’s look step-by-step what happens internally in the create_scm and simulate_system functions.

Let’s create a minimal example with a causal graph that has two nodes 0 and 1 and a single edge 1<-0. The signal we send through that graph is given by a Lorenz attractor with dimension 3 that is calculated for some time steps.

# Define parameters for a small test example
N_nodes = 2
N_timesteps = 1000
N_dimensions = 3

# Generate Lorenz attractor trajectory
d_lorenz = solve_system(N_timesteps, N_nodes, "Lorenz")
# Sample the simplest possible adjacency matrix: 0<--1
A = GrowingNetworkWithRedirection(N_nodes).generate()
A
tensor([[0., 0.],
        [1., 0.]])
# Sample random weights for the MLPs
W = initialize_weights(N_nodes, N_dimensions)
W
tensor([[[-0.2657, -1.0421, -1.8103],
         [-1.6468,  0.9513, -1.7132],
         [ 1.4713, -0.0967,  1.5466]],

        [[ 0.2724,  0.1536, -0.5448],
         [-0.2377, -1.2648,  0.5678],
         [ 1.5151, -1.0185,  1.1759]]])
# Sample random biases for the MLPs
b = initialize_biases(N_nodes, N_dimensions)
b
tensor([[ 1.4435,  1.7959,  0.0942],
        [-1.0453, -1.0797,  2.7330]])

Let’s plot the minimal test graph 1<–0 and propagate the Lorenz attractor through the causal graph.

First, nitialize the root node 1 with the time-series data from the Lorenz attractor. Then, propagate this signal through the edge 1->0. The signal is the input to the sampled multilayer perceptron (MLP) with no activation function. The output is the state of node 0.

# Initialize the root nodes with the Lorenz attractor
init = initialize_x(d_lorenz, A)

# Propagate Lorenz trajectory through the SCM
x = propagate_mlp(A, W, b, init=init)

# Visualize the results using xarray
data = xr.DataArray(x, dims=['time', 'node', "dim"])
root_nodes = get_root_nodes_mask(A)

plot_scm(G=create_scm_graph(A), root_nodes=root_nodes)
plot_3d_trajectories(data, root_nodes, line_alpha=1.)
plot_trajectories(data, root_nodes=root_nodes, sharey=False)
plt.show()
../_images/4bf133b74bb57d1a3a005f5063480f7d23164ac12842caae499e84b84576e3c0.png ../_images/cb7513c5c24715033c25b5d33f319c6abb59695fa433a14101ca6883b1137b73.png ../_images/fcea0345682fa919c2ee3517bf325f4512d66c8bdb35c3894b6bbe443e909263.png

We can even visualize the time unfolding of the system by creating animations. However, this may take a while to run.

# Create an animation of the simple coupled Lorenz attractor. 

# Increase the animation limit to 50MB
mpl.rcParams['animation.embed_limit'] = 50 * 1024**2 

da_sel = data.isel(time=slice(0, 500))
anim = animate_3d_trajectories(da_sel, frame_skip=2, rotate=True , show_history=True, plot_type='subplots', root_nodes=root_nodes)
display(anim)
INFO - Animation.save using <class 'matplotlib.animation.HTMLWriter'>

Specify graph structures#

You can also provide and explore specific graph structure. Let’s say we want to look at the SCM graph 1->0<-2

# Set parameters
num_nodes = 3
num_timesteps = 1000
dimensions = 3

A = torch.tensor([[0., 0., 0.],
                  [1., 0., 0.],
                  [1., 0., 0.]])
W = initialize_weights(num_nodes, dimensions)
b = initialize_biases(num_nodes, dimensions)
init = solve_system(num_timesteps, num_nodes, "Lorenz")
init2 = solve_system(num_timesteps, num_nodes, "Rossler")
init[:, 1] = init2[:, 1]
init = initialize_x(init, A)
x = propagate_mlp(A, W, b, init=init)
root_nodes = get_root_nodes_mask(A)
data = xr.DataArray(x, dims=['time', 'node', "dim"])

plot_scm(G=create_scm_graph(A), root_nodes=root_nodes)
plot_3d_trajectories(data, root_nodes, line_alpha=1.)
plot_trajectories(data, root_nodes=root_nodes, sharey=False)
plt.show()
../_images/a8e5ec1cfae77f07034dea48ca01845f3401a403066fbd2ea4ceffc345ab4d79.png ../_images/30bc88c5ae1c724ea3ca80db3d7db3ec8a582b15ce109574d4ae8baeae1c986b.png ../_images/7534b0b7fbfa4bd89a281a881372524994a3e90e2733a2371640617bd2979e4c.png

Now, we look at a linear chain: 2->1->0

# Sample the SCM, all hyperoparameters and propagate the Lorenz attractor through the SCM
A = torch.tensor([[0., 0., 0.],
        [1., 0., 0.],
        [0., 1., 0.]])
W = initialize_weights(num_nodes, dimensions)
b = initialize_biases(num_nodes, dimensions)
init = solve_system(num_timesteps, num_nodes, "Lorenz")
init = initialize_x(init, A)
x = propagate_mlp(A, W, b, init=init)
root_nodes = get_root_nodes_mask(A)
data = xr.DataArray(x, dims=['time', 'node', "dim"])

plot_scm(G=create_scm_graph(A), root_nodes=root_nodes)
plot_3d_trajectories(data, root_nodes, line_alpha=1.)
plot_trajectories(data, root_nodes, sharey=False)
plt.show()
../_images/1020b3b0d0366ca46d9de7ea918df16cff0101c726d41a423d5da20b073603d4.png ../_images/a1328bfb9573b2352874b057c20b771b84ad48ccd3ea95adfc113c0083423df3.png ../_images/bbf8517284f692e1d65ecd411cb0287bf3b8a4f20425e0f68e534c58267f2502.png